// Timekeeper - Method time tracker for program optimization.
// TimeData - Object to hold tracked information.
// Copyright (C) 2009  Robert N. Tenney
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

using System;
using System.Collections.Generic;
using RTenney.Utility;

namespace RTenney.Utility.Timekeeping {
    /// <summary>
    /// The Timekeeper class is a static class used to manage the method timers. Examples of usage
    /// are given with the Timekeeper.StartNew() static method.
    /// </summary>
    [CLSCompliantAttribute(true)]
    public static class Timekeeper {
        private static List<TimeData> timeList = new List<TimeData>();
        private static bool autoSave = true;
        private static string filename = "Timekeeper.log";
        private static string formatString = "##,##0.000000000000";
        private static int skip;
        private static bool noCallStack;
        private static string searchString;
        private static SortMethod sortMethod = SortMethod.LONGESTTIME;
        private static bool sorted;
        private static bool callStackLast = true;
        private static int longestLength = 0;

        /// <summary>
        /// Gets or sets the number of stack frames to skip. Use this if you are only interested
        /// in a particular class that is called by the main program.
        /// <example>
        /// Using this example, the timekeeper will skip the Program and MainForm stacks, and start
        /// tracking from the class called from the MainForm class.
        /// <code>Timekeeper.StacksToSkip = 2;
        /// </code>
        /// </example>
        /// </summary>
        public static int StacksToSkip {
            get {
                return skip;
            }
            set {
                skip = value;
            }
        }

        /// <summary>
        /// Gets or sets the flag to tell the Timekeeper to only track the Class and method that the
        /// timer is in. Defaults to false.
        /// </summary>
        public static bool NoCallStack {
            get {
                return noCallStack;
            }
            set {
                noCallStack = value;
            }
        }

        /// <summary>
        /// Gets or sets the format string to use for the time duration for output. Default is
        /// "##,##0.000000000000"
        /// </summary>
        public static string FormatString {
            get {
                return formatString;
            }
            set {
                formatString = value;
            }
        }

        /// <summary>
        /// Gets or sets a flag to tell the Timekeeper to automatically save the results to
        /// a file after processing the results with: Timekeeper.GenerateMessageList();
        /// Default is set to true.
        /// </summary>
        public static bool AutoSave {
            get {
                return autoSave;
            }
            set {
                autoSave = value;
            }
        }

        /// <summary>
        /// Gets or sets the path and filename to save the results to. The default is set to:
        /// "Timekeeper.log".
        /// </summary>
        public static string FileName {
            get {
                return filename;
            }
            set {
                filename = value;
            }
        }
        /// <summary>
        /// Creates a new TimeData instance, as well as starting the timer attached to the TimeData
        /// object.
        /// <example>
        /// Declare the timer:
        /// <code>
        /// TimeData timer;
        /// </code>
        /// Start the timer:
        /// <code>
        /// timer = Timekeeper.StartNew();
        /// </code>
        /// End the timer:
        /// <code>
        /// Timekeeper.End(timer);
        /// Timekeeper.End("Main form loader", timer); // replaces the timer's Signature with the string
        /// </code>
        /// </example>
        /// </summary>
        /// <returns>Returns the TimeData instance that was created. Note: upon creation of the
        /// instance, the timer begins.</returns>
        public static TimeData StartNew(string title) {
            TimeData temp = new TimeData(title);
            temp.Timer.Start();
            return temp;
        }

        public static TimeData StartNew() {
            return Timekeeper.StartNew("");
        }

        /// <summary>
        /// Calling this with an active TimeData object will stop the timer and add the results
        /// to the current Timekeeper tally for that method. Title will let you specify a title
        /// to output rather than the method call signature.
        /// <example>
        /// Example of using a title for the main form load timer:
        /// <code>
        ///         void MainFormLoad(object sender, EventArgs e)
        ///        {
        ///            MainFormTimer = Timekeeper.StartNew();
        ///        }
        ///
        ///        void MainFormShown(object sender, EventArgs e)
        ///        {
        ///            Timekeeper.End("MainForm Loader", MainFormTimer);
        ///            processMessages();
        ///        }
        ///
        /// // Outputs: -MainForm Loader |  Called: 1 time(s), Total time: .0061264, Average time: .0061264
        /// // Time durations will vary.
        /// </code>
        /// </example>
        /// </summary>
        /// <param name="timer">Active TimeData object to end.</param>
        public static void End(TimeData timer) {
            searchString = timer.Signature;
            timer.Timer.Stop();
            timer.AddToTime();
            if (timer.Title != "") {
                longestLength = Math.Max(timer.Title.Length, longestLength);
            } else {
                longestLength = Math.Max((timer.ClassName + "." + timer.MethodName).Length, longestLength);

            }
            int index;
            if ((index = timeList.FindIndex(findSignature)) != -1) {
                timeList[index].AddToTime(timer.Time);
                //timeList[index].Signature = title;
                timeList[index].CallCount++;
            } else {
                timer.CallCount++;
                //timer.Signature = title;
                timeList.Add(timer);
            }
        }

        /// <summary>
        /// Gets or sets the method of sorting to use by default.
        /// </summary>
        public static SortMethod DefaultSortMethod {
            get {
                return sortMethod;
            }
            set {
                sortMethod = value;
            }
        }

        /// <summary>
        /// Sort the tracked list with the given sort method.
        /// </summary>
        /// <param name="method">Method of sorting to use.</param>
        public static void Sort(SortMethod method) {
            TimeDataDurationComparer durationComparer = new TimeDataDurationComparer();
            TimeDataSignatureComparer signatureComparer = new TimeDataSignatureComparer();
            TimeDataMethodComparer methodComparer = new TimeDataMethodComparer();
            sorted = true;
            switch (method) {
            case SortMethod.SHORTESTTIME:
                timeList.Sort(durationComparer);
                break;
            case SortMethod.SIGNATURE:
                timeList.Sort(signatureComparer);
                break;
            case SortMethod.LONGESTTIME:
                timeList.Sort(durationComparer);
                timeList.Reverse();
                break;
            case SortMethod.REVERSESIGNATURE:
                timeList.Sort(signatureComparer);
                timeList.Reverse();
                break;
            case SortMethod.METHODID:
                timeList.Sort(methodComparer);
                break;
            case SortMethod.REVERSEMETHODID:
                timeList.Sort(methodComparer);
                timeList.Reverse();
                break;
            }
        }

        private static bool findSignature(TimeData data) {
            return data.Signature == searchString;
        }

        //        /// <summary>
        //        /// Calling this with an active TimeData object will stop the timer and add the results
        //        /// to the current Timekeeper tally for that method.
        //        /// </summary>
        //        /// <param name="timer">Active TimeData object to end.</param>
        //        public static void End(TimeData timer){
        //            End(timer.Signature, timer);
        //        }

        /// <summary>
        /// Generates a TimekeeperData list in the MessageHandler class with the results of
        /// all timers tracked by the Timekeeper. If the AutoSave property is set to true (default)
        /// it will automatically call the SaveList() method.
        /// </summary>
        public static void GenerateMessageList() {
            if (!sorted) {
                Sort(sortMethod);
            }
            foreach (TimeData item in timeList) {
                //TimeData time = item.Value;
                MessagesHandler.Add(MessageType.TIMEKEEPERDATA, entryString(item));
            }
            if (autoSave) {
                SaveList();
            }
        }


        /// <summary>
        /// Helper method to return the output string from the current DataTime item.
        /// </summary>
        /// <param name="time">The item.</param>
        /// <returns></returns>
        private static string entryString(TimeData time) {
            string ender = ",  ";
            string itemFirst;
            string itemLast;
            string classId = time.ClassName + "." + time.MethodName + ": ";
            if (callStackLast) {
                if (time.Title != "") {
                    itemFirst = time.Title;
                    itemLast = "";
                } else {
                    itemFirst = classId.PadRight(longestLength + 2);
                    itemLast = time.CallStackString;
                }
            } else {
                if (time.Title != "") {
                    itemFirst = time.Title;
                    itemLast = "";
                } else {
                    itemLast = classId.PadRight(longestLength+ 2);
                    itemFirst = time.CallStackString;
                }
            }
            if (time.CallCount > 1) {
                ender = "s, ";
            }
            System.Globalization.CultureInfo culture= new System.Globalization.CultureInfo("en-US", true);
            if (sortMethod.ToString().EndsWith("TIME")) {
                return "-Total time: " + time.Time.ToString(formatString, culture) + " Called: "
                       + time.CallCount + " time" + ender + "Average time: "
                       + time.AverageTime.ToString(formatString, culture) + "| " + itemFirst + itemLast;
            } else {
                return "-" + itemFirst + "|  Called: " + time.CallCount + " time" + ender + "Total time: "
                       + time.Time.ToString(formatString, culture) + ", Average time: "
                       + time.AverageTime.ToString(formatString, culture) + " | " + itemLast;
            }
        }

        /// <summary>
        /// Saves the results of the Timekeeper's tracker to disk using the previously defined
        /// path and filename, or the default if none was defined.
        /// </summary>
        public static void SaveList() {
            SaveList(filename);
        }

        /// <summary>
        /// Saves the results of the Timekeeper's tracker to disk using the provided path and
        /// filename.
        /// </summary>
        /// <param name="fileName">Path and filename to save the results to.</param>
        public static void SaveList(string fileName) {
            if (!sorted) {
                Sort(sortMethod);
            }
            System.IO.StreamWriter sw = new System.IO.StreamWriter(fileName);
            foreach (TimeData item in timeList) {
                //TimeData time = item.Value;
                sw.WriteLine(entryString(item));
            }
            sw.Close();
        }



        /// <summary>
        /// Methods to use for sorting the tracked list.
        /// </summary>
        public enum SortMethod {
            ///<summary>
            /// Sort by the method's signature, alphabetically.
            /// </summary>
            SIGNATURE,
            /// <summary>
            /// Sort by the method's total time, from shortest to longest.
            /// </summary>
            SHORTESTTIME,
            /// <summary>
            /// Sort by the method's signature, reverse alphabetically.
            /// </summary>
            REVERSESIGNATURE,
            /// <summary>
            /// Sort by the method's total time, from longest to shortest.
            /// </summary>
            LONGESTTIME, 
            METHODID,
            REVERSEMETHODID
        }
    }

    /// <summary>
    /// The TimeData instance holds all of the information for the Timekeeper to track. The
    /// TimeData class is not static and is generated automatically by the Timekeeper's StartNew()
    /// method. This class also contains the high precision timer used to measure the duration.
    /// </summary>
    public class TimeData : IDisposable {
        private int callCount;
        private double time;
        private double shortestTime = Double.MaxValue;
        private double longestTime;
        private HighPrecisionTimer timer;
        private string signature;
        private string className;
        private string methodName;
        private List<string> callStack = new List<string>();
        private string title;
        private string callStackString;

        /// <summary>
        /// Constructor for the TimeData class. This constructor takes a string parameter that
        /// identifies this instance. This constructor is called automatically be the Timekeeper's
        /// StartNew() method and the method's class and call stack is passed to this constructor
        /// by the Timekeeper.
        /// </summary>
        public TimeData(string title) {
            getInfo();
            this.title = title;
            timer = new HighPrecisionTimer();
        }

        public TimeData():this("") {

        }

        /// <summary>
        /// Helper method to retrieve the reflection information on the current method and
        /// class with the call stack.
        /// </summary>
        /// <returns>A string representation of the reflected information.</returns>
        private void getInfo() {
            string methodName = "";
            string className = "";
            string last = "";
            signature = "";
            System.Diagnostics.StackFrame[] calledBy = new System.Diagnostics.StackTrace().GetFrames();
            int startFrom = calledBy.GetLength(0) - 1 - Timekeeper.StacksToSkip;
            if (Timekeeper.NoCallStack) {
                startFrom = 2;
            }
            for (int iter = startFrom; iter > 3; iter--) {
                System.Diagnostics.StackFrame item = calledBy[iter];
                className = item.GetMethod().DeclaringType.Name;
                methodName = item.GetMethod().Name;
                if (item.GetMethod().IsConstructor) {
                    methodName = "Constructor";
                }
                string paramaterList = "";
                System.Reflection.ParameterInfo[] parameters = item.GetMethod().GetParameters();
                foreach (System.Reflection.ParameterInfo paramater in parameters) {
                    paramaterList += paramater.ToString() + ", ";
                }
                if (paramaterList.Length != 0) {
                    paramaterList = paramaterList.Remove(paramaterList.Length - 2, 2);
                }
                methodName += "(" + paramaterList + ")";
                string temp = className + "." + methodName;
                callStack.Add(temp);
                this.signature += temp + ":";
                last = temp + " => ";
                this.callStackString += last;
            }
            callStack.RemoveAt(callStack.Count - 1);
            this.className = className;
            this.methodName = methodName;
            this.signature = this.signature.Remove(this.signature.Length - 1, 1);
            int removeAmnt = (" => " + last).Length;
            this.callStackString = this.callStackString.Remove(this.callStackString.Length - removeAmnt);
        }

        /// <summary>
        /// Gets and sets the High Precision Timer to use for this object. Set automatically by
        /// the constructor.
        /// </summary>
        public HighPrecisionTimer Timer {
            get {
                return this.timer;
            }
            set {
                this.timer = value;
            }
        }

        /// <summary>
        /// Gets and sets the number of times the associated method is called. Set automatically by
        /// the Timekeeper.
        /// </summary>
        public int CallCount {
            get {
                return this.callCount;
            }
            set {
                this.callCount = value;
            }
        }

        public List<string> CallStack {
            get {
                return this.callStack;
            }
        }

        public string CallStackString {
            get {
                return this.callStackString;
            }
        }

        public string Title {
            get {
                return this.title;
            }
        }

        public string ClassName {
            get {
                return this.className;
            }
        }

        public string MethodName {
            get {
                return this.methodName;
            }
        }

        /// <summary>
        /// Gets the total time duration for the associated method.
        /// </summary>
        public double Time {
            get {
                return this.time;
            }
        }

        /// <summary>
        /// Gets the average time duration for the calls to the associated method.
        /// </summary>
        public double AverageTime {
            get {
                return this.time / (double)this.callCount;
            }
        }

        /// <summary>
        /// Adds a specified time duration to the current time duration. THis allows the user
        /// to override the automatic time tracker.
        /// </summary>
        /// <param name="value">Value in double to add to the current time duration.</param>
        public void AddToTime(double value) {
            this.time += value;
            if (shortestTime > value) {
                shortestTime = value;
            } else if (longestTime<value) {
                longestTime = value;
            }
        }

        /// <summary>
        /// Adds the last measured time to the total time duration. This is called automatically
        /// by the Timekeeper.
        /// </summary>
        public void AddToTime() {
            AddToTime(this.timer.Duration);
        }

        /// <summary>
        /// Retrieves the signature, or string representation of the method, class, and call stack being
        /// tracked by this instance.
        /// </summary>
        public string Signature {
            get {
                return this.signature;
            }
            set {
                this.signature = value;
            }
        }

        /// <summary>
        /// Returns the Signature property.
        /// </summary>
        /// <returns></returns>
        public override string ToString() {
            if (this.title != "") {
                return this.title;
            } else {
                return this.signature;
            }
        }

        /// <summary>
        /// Compares this object to another to determine if they are equal.
        /// </summary>
        /// <param name="obj">Object to compare to.</param>
        /// <returns>True if the object is the same, false otherwise.</returns>
        public override bool Equals(object obj) {
            if (obj is TimeData) {
                return ((TimeData)obj).Signature == this.Signature;
            }
            return false;
        }

        /// <summary>
        /// Generates a unique hashcode for this object.
        /// </summary>
        /// <returns>The hashcode as an integer.</returns>
        public override int GetHashCode() {
            int hcs = this.Signature.GetHashCode();


            return hcs;
        }

        /// <summary>
        /// Compares two MessageData objects to determine if they are the same.
        /// </summary>
        /// <param name="dat1">The first object.</param>
        /// <param name="dat2">The second object.</param>
        /// <returns>True if they are the same, false otherwise.</returns>
        public static bool operator ==(TimeData dat1, TimeData dat2) {
            return dat1.Equals(dat2);
        }

        /// <summary>
        /// Compares if two MessageData objects are not the same.
        /// </summary>
        /// <param name="dat1">The first object.</param>
        /// <param name="dat2">The second object.</param>
        /// <returns>True if they are not the same, false otherwise.</returns>
        public static bool operator !=(TimeData dat1, TimeData dat2) {
            return !dat1.Equals(dat2);
        }

        public void Dispose() {
            Timekeeper.End(this);
        }
    }


}
